﻿namespace OasisCiq.XS
{
    using System;
    using System.Globalization;
    using System.Text;

    public class XSDuration
    {
        public XSDuration()
        {
            Sign = 1;
        }

        public XSDuration(string value)
        {
            XSDuration arg;
            if (!TryParse(value, out arg))
            {
                throw new ArgumentException();
            }

            Sign = arg.Sign;
            Years = arg.Years;
            Months = arg.Months;
            Days = arg.Days;
            Hours = arg.Hours;
            Minutes = arg.Minutes;
            Seconds = arg.Seconds;
            Milliseconds = arg.Milliseconds;
        }

        public int Sign { get; set; }

        public int Years { get; set; }

        public int Months { get; set; }

        public int Days { get; set; }

        public int Hours { get; set; }

        public int Minutes { get; set; }

        public int Seconds { get; set; }

        public int Milliseconds { get; set; }

        public static bool TryParse(string value, out XSDuration result)
        {
            result = new XSDuration();
            if (string.IsNullOrEmpty(value) || !(value.StartsWith("P") || value.StartsWith("-P")))
            {
                return false;
            }

            const string states = "-PYMDTHMS";
            var digits = new StringBuilder(5);
            var state = 0;
            var timePart = false;
            foreach (var ch in value)
            {
                if (char.IsDigit(ch) || ch == '.')
                {
                    digits.Append(ch);
                }
                else
                {
                    // move to next state
                    while (ch != states[state])
                    {
                        state++;
                        if (state == states.Length)
                        {
                            return false;
                        }
                    }

                    switch (ch)
                    {
                        case '-':
                        case 'P':
                        case 'T':
                            if (digits.Length > 0)
                            {
                                return false;
                            }

                            break;
                        case 'Y':
                        case 'M':
                        case 'D':
                        case 'H':
                        case 'S':
                            if (digits.Length == 0)
                            {
                                return false;
                            }

                            break;
                        default:
                            return false;
                    }

                    try
                    {
                        switch (ch)
                        {
                            case '-':
                                result.Sign = -1;
                                break;
                            case 'P':
                                break;
                            case 'Y':
                                result.Years = int.Parse(digits.ToString());
                                break;
                            case 'M':
                                if (timePart)
                                {
                                    result.Minutes = int.Parse(digits.ToString());
                                }
                                else
                                {
                                    result.Months = int.Parse(digits.ToString());
                                }

                                break;
                            case 'D':
                                result.Days = int.Parse(digits.ToString());
                                break;
                            case 'T':
                                timePart = true;
                                break;
                            case 'H':
                                result.Hours = int.Parse(digits.ToString());
                                break;
                            case 'S':
                                var seconds = double.Parse(
                                    digits.ToString(), 
                                    CultureInfo.InvariantCulture);
                                var wholeSeconds = Math.Floor(seconds);
                                seconds -= wholeSeconds;
                                var milliseconds = Math.Round(seconds * 1000);
                                result.Seconds = (int)wholeSeconds;
                                result.Milliseconds = (int)milliseconds;
                                break;

                            // default: see previous switch;
                        }
                    }
                    catch (FormatException)
                    {
                        return false;
                    }
                    catch (OverflowException)
                    {
                        return false;
                    }

                    digits.Length = 0;
                }
            }

            return digits.Length == 0;
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            if (Sign < 0)
            {
                sb.Append('-');
            }

            sb.Append('P');
            if (Years > 0)
            {
                sb.Append(Years.ToString(CultureInfo.InvariantCulture));
                sb.Append('Y');
            }

            if (Months > 0)
            {
                sb.Append(Months.ToString(CultureInfo.InvariantCulture));
                sb.Append('M');
            }

            if (Days > 0)
            {
                sb.Append(Days.ToString(CultureInfo.InvariantCulture));
                sb.Append('D');
            }

            if (Hours > 0 || Minutes > 0 || Seconds > 0 || Milliseconds > 0)
            {
                sb.Append('T');
            }
            else
            {
                return sb.ToString();
            }

            if (Hours > 0)
            {
                sb.Append(Hours.ToString(CultureInfo.InvariantCulture));
                sb.Append('H');
            }

            if (Minutes > 0)
            {
                sb.Append(Minutes.ToString(CultureInfo.InvariantCulture));
                sb.Append('M');
            }

            if (Seconds > 0 || Milliseconds > 0)
            {
                if (Seconds == 0)
                {
                    sb.Append('0');
                }
                else
                {
                    sb.Append(Seconds.ToString(CultureInfo.InvariantCulture));
                }

                if (Milliseconds > 0)
                {
                    sb.Append('.');
                    var msecStr = Milliseconds.ToString(CultureInfo.InvariantCulture);
                    sb.Append(msecStr.TrimEnd('0'));
                }

                sb.Append('S');
            }

            return sb.ToString();
        }

        public TimeSpan ToTimeSpan()
        {
            // Code from http://lordzoltan.blogspot.com/2010/09/map-systemtimespan-to-xsduration-for.html

            /* now have to split the seconds into whole and fractional.
            note - there is clearly a potential for a loss of fidelity
            here given that we're expanding years and months to 365 and
            30 days respectively. There's no perfect solution - although
            you can simply ask your web service clients to express all
            durations in terms of days, hours, minutes and seconds. */

            var totalDays = (Years * 365) + (Months * 30) + Days;
            var ts = new TimeSpan(totalDays, Hours, Minutes, Seconds, Milliseconds);
            if (Sign < 0)
            {
                ts = ts.Negate();
            }

            return ts;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>Total days as <see cref="System.TimeSpan"/></returns>
        public TimeSpan GetTimeSpanPrecision()
        {
            // from here: http://www.w3.org/TR/xmlschema-2/#duration-order
            throw new NotImplementedException();
        }
    }
}
